import re
from calendar import timegm
from datetime import timedelta, datetime, UTC
from typing import Optional

import bcrypt
from fastapi import status, Depends, Request, WebSocket
from fastapi.encoders import jsonable_encoder
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
from httpx import AsyncClient, Timeout, ConnectError, ConnectTimeout
from jose import jwt
from pydantic import ValidationError

from rosa.scarborough_fair.depends.authorization.types import PayloadData, TokenType, JwtError, ScopeError, NoPermissionError, UserAccessProfile, UserAccessProfileCacheData
from rosa.scarborough_fair.depends.cache.cache import CacheSystem, cache
from rosa.scarborough_fair.generator_setting.settings import settings, logger
from rosa.scarborough_fair.status.global_status import StatusMap
from rosa.scarborough_fair.status.types.exception import SiteException
from rosa.scarborough_fair.status.types.response import Status, BaseResponse


class PermissionHandler:
    functions = dict()
    default_func_name = "default"

    def register(self, name: str, func: callable):
        if len(self.functions) == 0:
            self.default_func_name = name

        self.functions[name] = func

    # 根据名称获取函数，有则返回函数，没有则返回None，如果设置了标记位，则返回第一个函数
    def get(self, name: str, flag: bool = False):
        if name in self.functions:
            return self.functions[name]
        elif flag:
            return self.functions.get(self.default_func_name)

        return None

    async def exec_handler(self, name: str, flag: bool = False, user_id: int = 0, token_type: str = "local") -> UserAccessProfile:
        """
        执行注册的函数
        :param name: 函数名称
        :param flag: 是否执行默认函数
        :param user_id: 用户id,处理函数需要的参数
        :param token_type: token类型,处理函数需要的参数
        """
        handler = self.get(name, flag)
        if handler:
            result = await handler(user_id, token_type)
            if isinstance(result, UserAccessProfile):
                return result
        else:
            return UserAccessProfile()

    async def save(self, user_id: int, token_type='local', user_access_profile: Optional[UserAccessProfile] = None, expire: int = 0):
        """
        保存权限信息到缓存

        :param user_id: 用户id
        :param token_type: token类型
        :param user_access_profile: 用户访问配置档案
        :param expire: 过期时间
        """
        if not user_access_profile:
            user_access_profile = self.exec_handler(self.default_func_name, True, user_id, token_type)

        # 将权限信息转换为字典格式
        access_profile_dict = UserAccessProfileCacheData()

        for permission in user_access_profile.permissions:
            getattr(access_profile_dict.permissions, permission.method).append(permission)

        # # 如果权限为空，则删除该key，即空访问方法
        # for method in access_profile_dict["permissions"]:
        #     if not access_profile_dict["permissions"][method]:
        #         del access_profile_dict["permissions"][method]
        # 是否是超级管理员的信息也存储到权限信息中
        access_profile_dict.superuser = user_access_profile.superuser
        # 菜单信息
        access_profile_dict.menus = user_access_profile.menus

        cache_client = cache()
        if not expire:
            expire = settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
        await cache_client.set_permission(identify=user_id, permissions=access_profile_dict.model_dump(), expire=expire)


permission_handler = PermissionHandler()

token_url = "/cas/login"
if settings.AUTH_ENABLE and not settings.AUTH_ROUTER_ENABLE:
    if settings.AUTH_URL.startswith("http"):
        token_url = f"{settings.AUTH_URL}{token_url}"
    elif settings.AUTH_URL:
        token_url = f"/{settings.AUTH_URL}{token_url}"

oauth2_scheme = OAuth2PasswordBearer(tokenUrl=token_url)
optional_oauth2_scheme = OAuth2PasswordBearer(tokenUrl=token_url, auto_error=False)


def get_authorization_exception(status_item: Status, headers: dict | None = None) -> SiteException:
    """
    生成权限验证异常
    :param status_item:
    :param headers:
    :return:
    """
    response = BaseResponse(status=status_item)

    if headers is None:
        headers = {"WWW-Authenticate": "Bearer"}

    return SiteException(status_code=status.HTTP_401_UNAUTHORIZED, response=response, headers=headers)


# #####################
# ###### 密码加密 ######
# ####################
async def get_password_hash(password: bytes) -> bytes:
    salt = bcrypt.gensalt()
    return bcrypt.hashpw(password, salt)


async def verify_password(plain_password: bytes, hashed_password: bytes) -> bool:
    return bcrypt.checkpw(plain_password, hashed_password)


async def create_access_token(payload: PayloadData, expires_delta: timedelta = None, subject: TokenType = TokenType.TOKEN) -> str:
    """
    创建jwt格式token
    :param payload:
    :param expires_delta:
    :param subject:
    :return: token
    """
    if expires_delta is None:
        if settings.ACCESS_TOKEN_EXPIRE_MINUTES:
            expires_delta = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
        else:
            expires_delta = timedelta(minutes=15)

    expire = datetime.now(tz=UTC) + expires_delta

    # 生成payload
    # 过期时间
    payload.exp = timegm(expire.utctimetuple())
    # 生效时间
    payload.nbf = timegm((datetime.now(tz=UTC) - timedelta(seconds=10)).utctimetuple())
    # 签发时间
    payload.iat = timegm(datetime.now(tz=UTC).utctimetuple())
    # 签发者
    payload.iss = settings.SITE_NAME
    # 接收者
    payload.sub = subject.value
    payload.jti = f"{payload.aud}{payload.iat}"

    to_encode = {key: value for key, value in jsonable_encoder(payload).items() if value is not None}

    # 如果是远程认证并且认证路由没有开启，则远程创建token，否则本地创建token
    if settings.AUTH_TYPE == "remote" and not settings.AUTH_ROUTER_ENABLE:
        try:
            # 远程创建token
            async with AsyncClient(verify=False, timeout=Timeout(10)) as client:
                response = await client.post(f"{settings.AUTH_URL}/create", json={"payload": to_encode})
                if response.status_code != 200:
                    raise JwtError("远程创建token失败")
                encoded_jwt = response.json().get("token")
        except ConnectError as e:
            logger.exception(f"远程创建token失败 => {e}")
            raise JwtError("远程创建token失败，后端接口通信失败，请稍后再试")
        except ConnectTimeout as e:
            logger.exception(f"远程创建token失败 => {e}")
            raise JwtError("远程创建token失败，后端接口通信超时，请稍后再试")
    else:
        encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
    return encoded_jwt


async def permission_authentication(token: str, method: str, path: str, cache_client: CacheSystem, security_scopes: SecurityScopes) -> PayloadData:
    """
    验证权限
    :return:
    """
    payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM], options={"verify_aud": False})
    payload = PayloadData.model_validate(payload)
    if payload.data and payload.data.id:
        # 如果有黑名单记录查看是否符合条件，符合条件则不允许登录
        blacklist = await cache_client.get_blacklist(payload.data.id)
        if blacklist:
            blacklist = PayloadData.model_validate_json(blacklist)
            if blacklist.nbf > payload.iat:
                raise jwt.ExpiredSignatureError("token已经在黑名单中了")
    else:
        logger.info(f"jwt数据有问题 => {payload}/{token}")
        raise JwtError("jwt数据有问题")

    # 如果是超级管理员则不再进行权限验证，直接返回 payload 信息
    if payload.data.superuser:
        return payload

    # scopes权限验证
    can_active = payload.scopes
    for scope in security_scopes.scopes:
        # 方法权限有不能进入的项
        if scope not in can_active:
            logger.info(f"scope权限不足 => 安全权限:{security_scopes.scopes}/token申请权限:{can_active}")
            # raise get_authorization_exception(status_item=StatusMap.SCOPE_NOT_AUTHORIZED, headers=credentials_exception_headers)
            raise ScopeError("scope权限不足")

    # 访问路径需要去掉最后的/
    if path.endswith("/"):
        path = path[:-1]

    # 验证白名单列表，如果在白名单中则直接返回payload
    # TODO 白名单列表使用数据库存储
    for url_reg in settings.WEB_WHITE_TABLE.get(method, []):
        url_reg = f"^{url_reg}$"

        if re.match(url_reg, path):
            return payload

    # 如果不需要权限验证则直接返回payload
    if not settings.PERMISSION_ENABLE:
        return payload

    # 获取权限缓存
    permissions = await cache_client.get_permission(payload.data.id, method)

    if permissions is None:
        user_id = payload.data.id
        token_type = payload.data.type
        user_access_profile = await permission_handler.exec_handler(token_type, True, user_id, token_type)
        # if permission exists, save it to cache
        if user_access_profile.permissions:
            await permission_handler.save(user_id, token_type, user_access_profile)
            permissions = [permission.model_dump() for permission in user_access_profile.permissions if permission.method == method]
        else:
            permissions = []

    for permission in permissions:
        url_reg = f"^{permission.get('url')}$"

        if re.match(url_reg, path):
            return payload

    raise NoPermissionError("权限不足")


async def authentication_handler(request: Request, security_scopes: SecurityScopes, token: str, cache_client: CacheSystem) -> PayloadData:
    method = request.headers.get("X-Method")
    if method:
        method = method.upper()
    else:
        method = request.method.upper()
    path = request.headers.get("X-Path")
    if not path:
        path = request.url.path
    payload = await permission_authentication(token, method, path, cache_client, security_scopes)
    return payload


async def signature_authentication(
        request: Request,
        security_scopes: SecurityScopes,
        token: str = Depends(oauth2_scheme),
        cache_client: CacheSystem = Depends(cache)
) -> PayloadData:
    """
    验证权限(必须登录)
    :param request: 请求对象，获取权限的时候需要根据当前访问方法获取url列表
    :param security_scopes: 权限范围
    :param token: 传入token
    :param cache_client: 缓存客户端
    :return: payload 信息
    """
    if settings.AUTH_TYPE == "remote" and not settings.AUTH_ROUTER_ENABLE:
        try:
            # 远程验证
            path = request.url.path
            method = request.method.upper()
            async with AsyncClient(verify=False, timeout=Timeout(10)) as client:
                response = await client.post(f"{settings.AUTH_URL}/cas/verify", headers={"Authorization": f"Bearer {token}", "X-Path": path, "X-Method": method})
                if response.status_code != 200:
                    logger.error(f"远程验证失败 => {response.text}")
                    response_status = StatusMap.INVALIDATE_CREDENTIALS
                    response_data = BaseResponse(code=response_status.code, success=response_status.success, message=response_status.message)
                    raise SiteException(status_code=response.status_code, response=response_data)
                payload = PayloadData.model_validate(response.json())
        except ConnectError as e:
            logger.exception(f"远程验证失败 => 接口失败:{e}")
            response_status = StatusMap.INVALIDATE_CREDENTIALS
            response = BaseResponse(code=response_status.code, success=response_status.success, message="远程验证失败，后端接口通信失败，请稍后再试")
            raise SiteException(status_code=status.HTTP_401_UNAUTHORIZED, response=response)
        except ConnectTimeout as e:
            logger.exception(f"远程验证失败 => 接口超时:{e}")
            response_status = StatusMap.INVALIDATE_CREDENTIALS
            response = BaseResponse(code=response_status.code, success=response_status.success, message="远程验证失败，后端接口通信超时，请稍后再试")
            raise SiteException(status_code=status.HTTP_401_UNAUTHORIZED, response=response)
        return payload
    else:
        # 本地验证

        credentials_exception_headers = None
        if security_scopes.scopes:
            authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
            credentials_exception_headers = {'WWW-Authenticate': authenticate_value}
        try:
            return await authentication_handler(request, security_scopes, token, cache_client)
        except jwt.ExpiredSignatureError as e:
            logger.error(f"jwt 已经过期 => {token}[{e}]")
            raise get_authorization_exception(status_item=StatusMap.EXPIRED_CREDENTIALS)
        except (jwt.JWTError, ValidationError) as e:
            logger.error(f"jwt 数据处理有问题 => {token}[{e}]")
            raise get_authorization_exception(status_item=StatusMap.INVALIDATE_CREDENTIALS, headers=credentials_exception_headers)
        except JwtError as e:
            logger.error(f"jwt 数据处理有问题 => {token}[{e}]")
            raise get_authorization_exception(status_item=StatusMap.INVALIDATE_CREDENTIALS, headers=credentials_exception_headers)
        except ScopeError as e:
            logger.error(f"scope权限不足 => {e}")
            raise get_authorization_exception(status_item=StatusMap.SCOPE_NOT_AUTHORIZED, headers=credentials_exception_headers)
        except NoPermissionError:
            response_status = StatusMap.FORBIDDEN
            response = BaseResponse(code=response_status.code, success=response_status.success, message=response_status.message)
            raise SiteException(status_code=status.HTTP_403_FORBIDDEN, response=response)
        except Exception as e:
            logger.error(f"signature_authentication error => {e}")
            response_status = StatusMap.FORBIDDEN
            response = BaseResponse(code=response_status.code, success=response_status.success, message=response_status.message)
            raise SiteException(status_code=status.HTTP_403_FORBIDDEN, response=response)


async def optional_signature_authentication(
        request: Request,
        security_scopes: SecurityScopes,
        token: str = Depends(optional_oauth2_scheme),
        cache_client: CacheSystem = Depends(cache)
) -> PayloadData:
    """
    验证权限（可以不登录）
    :param request: 请求对象，获取权限的时候需要根据当前访问方法获取url列表
    :param security_scopes: 权限范围
    :param token: 传入token
    :param cache_client: 缓存客户端
    :return: payload 信息
    """
    payload = PayloadData()

    try:
        payload = await signature_authentication(request, security_scopes, token, cache_client)
    except NoPermissionError:
        response_status = StatusMap.FORBIDDEN
        response = BaseResponse(code=response_status.code, success=response_status.success, message=response_status.message)
        raise SiteException(status_code=status.HTTP_403_FORBIDDEN, response=response)
    except Exception as e:
        logger.debug(f"optional_signature_authentication => {e}")
    return payload


# websocket 验证
async def websocket_signature_authentication(websocket: WebSocket, security_scopes: SecurityScopes, cache_client: CacheSystem = Depends(cache)) -> PayloadData:
    # token 通过 Sec-WebSocket-Protocol 传递
    token = websocket.headers.get('Sec-WebSocket-Protocol')
    method = 'websocket'
    path = websocket.url.path
    try:
        # 验证权限，获取jwt的payload
        payload = await permission_authentication(token, method, path, cache_client, security_scopes)
        return payload
    except jwt.ExpiredSignatureError as e:
        logger.error(f"websocket jwt 已经过期 => {token}[{e}]")
    except (jwt.JWTError, ValidationError) as e:
        logger.error(f"websocket jwt 数据处理有问题 => {token}[{e}]")
    except JwtError as e:
        logger.info(f"websocket jwt数据有问题 => {token}[{e}]")
    except ScopeError as e:
        logger.error(f"websocket scope权限不足 => {e}")
    except NoPermissionError:
        logger.error(f"websocket 权限不足")
    await websocket.close()

    response_status = StatusMap.FORBIDDEN
    response = BaseResponse(code=response_status.code, success=response_status.success, message=response_status.message)
    raise SiteException(status_code=status.HTTP_403_FORBIDDEN, response=response)
